/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
    ArkAssignStmt,
    ArkInstanceFieldRef,
    Local,
    FieldSignature,
    ArkMethod,
    ArkClass,
    ArkInstanceInvokeExpr,
    Stmt,
    Value
} from 'arkanalyzer';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { Rule, Defects, MatcherTypes, MatcherCallback, ClassMatcher } from '../../Index';
import { IssueReport } from '../../model/Defects';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'UpdateStateVarBetweenAnimateTosCheck');
const gMetaData: BaseMetaData = {
    severity: 1,
    ruleDocPath: 'docs/update-state-var-between-animatetos-check.md',
    description: 'Avoid updating state variables between animateTo calls.',
};

interface WarnInfo {
    lineNum: number;
    startCol: number;
    endCol: number;
    filePath: string;
};

export class UpdateStateVarBetweenAnimateTosCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private classMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS,
        hasViewTree: true,
    };

    public registerMatchers(): MatcherCallback[] {
        const matchCb: MatcherCallback = {
            matcher: this.classMatcher,
            callback: this.check,
        };
        return [matchCb];
    }

    public check = (target: ArkClass): void => {
        const stateVars = new Set(
            target
                .getFields()
                .filter(f => f.hasDecorator('State'))
                .map(f => f.getSignature())
        );
        if (stateVars.size > 0) {
            target.getMethods().forEach(m => this.checkMethod(m, stateVars));
            
        }
    };

    private checkMethod(target: ArkMethod, stateVars: Set<FieldSignature>): void {
        let aliases = new Set<Local>();
        let animateToFlag = { value: false };
        let issues: IssueReport[] = [];
        const stmts = target.getBody()?.getCfg().getStmts() ?? [];
        for (const stmt of stmts) {
            this.checkAnimateTo(stmt, animateToFlag, issues);
            if (!(stmt instanceof ArkAssignStmt)) {
                continue;
            }
            if (this.isAssignToStateVar(stmt, stateVars, aliases)) {
                issues.push(this.createIssueReport(stmt, stmt.getLeftOp()));
            } else {
                const alias = this.isAliasOfStateVar(stmt, stateVars, aliases);
                if (alias) {
                    aliases.add(alias);
                }
            }
        }
    }

    private checkAnimateTo(stmt: Stmt, animateToFlag: { value: boolean }, issues: IssueReport[]): void {
        const invoke = stmt.getInvokeExpr();
        if (!invoke || !(invoke instanceof ArkInstanceInvokeExpr)) {
            return;
        }
        const calleeSig = invoke.getMethodSignature();
        if (calleeSig.getMethodSubSignature().getMethodName() !== 'animateTo') {
            return;
        }
        if (animateToFlag.value) {
            // 之前已经遇到了 animateTo，现在是两个 animateTo 之间
            issues.forEach(i => this.issues.push(i));
            issues.length = 0;
        } else {
            // 第一次遇到 animateTo，之前对状态变量的修改都是合理的，应该清空
            animateToFlag.value = true;
            issues.length = 0;
        }
        
    }

    private isAssignToStateVar(stmt: ArkAssignStmt, stateVars: Set<FieldSignature>, aliases: Set<Local>): boolean {
        const leftOp = stmt.getLeftOp();
        if (leftOp instanceof ArkInstanceFieldRef) {
            // this.n = 1 or this.obj.n = 1
            return stateVars.has(leftOp.getFieldSignature()) || aliases.has(leftOp.getBase());
        } else if (leftOp instanceof Local) {
            return aliases.has(leftOp);
        }
        return false;
    }

    private isAliasOfStateVar(
        stmt: ArkAssignStmt,
        stateVars: Set<FieldSignature>,
        aliases: Set<Local>
    ): Local | undefined {
        const leftOp = stmt.getLeftOp();
        const rightOp = stmt.getRightOp();
        if (leftOp instanceof Local && rightOp instanceof ArkInstanceFieldRef) {
            // %1 = this.n
            // or
            // %1 = this.obj; %2 = %1.n
            if (stateVars.has(rightOp.getFieldSignature()) || aliases.has(rightOp.getBase())) {
                return leftOp;
            }
        }
        return undefined;
    }

    private createIssueReport(stmt: Stmt, operand: Value): IssueReport {
        const severity = this.rule.alert ?? this.metaData.severity;
        const warnInfo = this.getLineAndColumn(stmt, operand);
        let defects = new Defects(
            warnInfo.lineNum,
            warnInfo.startCol,
            warnInfo.endCol,
            this.metaData.description,
            severity,
            this.rule.ruleId,
            warnInfo.filePath,
            this.metaData.ruleDocPath,
            true,
            false,
            false
        );
        return new IssueReport(defects, undefined);
    }

    private getLineAndColumn(stmt: Stmt, operand: Value): WarnInfo {
        const arkFile = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile();
        const originPosition = stmt.getOperandOriginalPosition(operand);
        if (arkFile && originPosition) {
            const originPath = arkFile.getFilePath();
            const lineNum = originPosition.getFirstLine();
            const startCol = originPosition.getFirstCol();
            const endCol = startCol;
            return { lineNum, startCol, endCol, filePath: originPath };
        } else {
            logger.debug('ArkFile is null.');
        }
        return { lineNum: -1, startCol: -1, endCol: -1, filePath: '' };
    }
}
